iT邦幫忙

2024 iThome 鐵人賽

DAY 23
0
Modern Web

React 學得動嗎系列 第 23

[Day 23] Gym Pro:打造報表和分析功能

  • 分享至 

  • xImage
  •  

今天,我們要來實作一個報表和分析功能,讓健身房老闆可以一目了然地看到整個健身房的運營狀況。

1. 安裝必要的套件

首先,我們需要安裝 Recharts,這是一個強大的 React 圖表庫:

npm install recharts

2. 建立儀表板頁面

讓我們來更新我們的儀表板頁面,加入一些重要的統計數據和圖表。

src/pages/Dashboard.tsx 中:

import React from 'react';
import { useQuery } from '@tanstack/react-query';
import axios from 'axios';
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, Legend, ResponsiveContainer, BarChart, Bar, PieChart, Pie, Cell } from 'recharts';

interface DashboardData {
  totalMembers: number;
  totalClasses: number;
  revenueThisMonth: number;
  memberGrowth: { month: string; members: number }[];
  classAttendance: { className: string; attendees: number }[];
  membershipTypes: { type: string; count: number }[];
}

const fetchDashboardData = async (): Promise<DashboardData> => {
  const { data } = await axios.get<DashboardData>('/api/dashboard');
  return data;
};

const COLORS = ['#0088FE', '#00C49F', '#FFBB28', '#FF8042'];

const Dashboard: React.FC = () => {
  const { data, isLoading, error } = useQuery(['dashboardData'], fetchDashboardData);

  if (isLoading) return <div>載入中...</div>;
  if (error) return <div>發生錯誤:{(error as Error).message}</div>;

  return (
    <div className="p-4">
      <h1 className="text-2xl font-bold mb-4">儀表板</h1>
      
      <div className="grid grid-cols-1 md:grid-cols-3 gap-4 mb-8">
        <Card>
          <CardHeader>
            <CardTitle>總會員數</CardTitle>
          </CardHeader>
          <CardContent>
            <p className="text-3xl font-bold">{data?.totalMembers}</p>
          </CardContent>
        </Card>
        <Card>
          <CardHeader>
            <CardTitle>總課程數</CardTitle>
          </CardHeader>
          <CardContent>
            <p className="text-3xl font-bold">{data?.totalClasses}</p>
          </CardContent>
        </Card>
        <Card>
          <CardHeader>
            <CardTitle>本月營收</CardTitle>
          </CardHeader>
          <CardContent>
            <p className="text-3xl font-bold">${data?.revenueThisMonth}</p>
          </CardContent>
        </Card>
      </div>

      <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
        <Card>
          <CardHeader>
            <CardTitle>會員成長趨勢</CardTitle>
          </CardHeader>
          <CardContent>
            <ResponsiveContainer width="100%" height={300}>
              <LineChart data={data?.memberGrowth}>
                <CartesianGrid strokeDasharray="3 3" />
                <XAxis dataKey="month" />
                <YAxis />
                <Tooltip />
                <Legend />
                <Line type="monotone" dataKey="members" stroke="#8884d8" />
              </LineChart>
            </ResponsiveContainer>
          </CardContent>
        </Card>

        <Card>
          <CardHeader>
            <CardTitle>課程參與度</CardTitle>
          </CardHeader>
          <CardContent>
            <ResponsiveContainer width="100%" height={300}>
              <BarChart data={data?.classAttendance}>
                <CartesianGrid strokeDasharray="3 3" />
                <XAxis dataKey="className" />
                <YAxis />
                <Tooltip />
                <Legend />
                <Bar dataKey="attendees" fill="#82ca9d" />
              </BarChart>
            </ResponsiveContainer>
          </CardContent>
        </Card>

        <Card>
          <CardHeader>
            <CardTitle>會員類型分佈</CardTitle>
          </CardHeader>
          <CardContent>
            <ResponsiveContainer width="100%" height={300}>
              <PieChart>
                <Pie
                  data={data?.membershipTypes}
                  cx="50%"
                  cy="50%"
                  labelLine={false}
                  outerRadius={80}
                  fill="#8884d8"
                  dataKey="count"
                  label={({ name, percent }) => `${name} ${(percent * 100).toFixed(0)}%`}
                >
                  {data?.membershipTypes.map((entry, index) => (
                    <Cell key={`cell-${index}`} fill={COLORS[index % COLORS.length]} />
                  ))}
                </Pie>
                <Tooltip />
              </PieChart>
            </ResponsiveContainer>
          </CardContent>
        </Card>
      </div>
    </div>
  );
};

export default Dashboard;

3. 建立報表頁面

接下來,我們來建立一個專門的報表頁面,提供更詳細的數據分析。

src/pages/Reports.tsx 中:

import React, { useState } from 'react';
import { useQuery } from '@tanstack/react-query';
import axios from 'axios';
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, Legend, ResponsiveContainer } from 'recharts';

interface ReportData {
  revenue: { date: string; amount: number }[];
  newMembers: { date: string; count: number }[];
  classAttendance: { date: string; count: number }[];
}

const fetchReportData = async (period: string): Promise<ReportData> => {
  const { data } = await axios.get<ReportData>(`/api/reports?period=${period}`);
  return data;
};

const Reports: React.FC = () => {
  const [period, setPeriod] = useState('month');
  const { data, isLoading, error } = useQuery(['reportData', period], () => fetchReportData(period));

  if (isLoading) return <div>載入中...</div>;
  if (error) return <div>發生錯誤:{(error as Error).message}</div>;

  return (
    <div className="p-4">
      <h1 className="text-2xl font-bold mb-4">報表</h1>
      
      <div className="mb-4">
        <Select value={period} onValueChange={setPeriod}>
          <SelectTrigger className="w-[180px]">
            <SelectValue placeholder="選擇時間範圍" />
          </SelectTrigger>
          <SelectContent>
            <SelectItem value="week">本週</SelectItem>
            <SelectItem value="month">本月</SelectItem>
            <SelectItem value="year">本年</SelectItem>
          </SelectContent>
        </Select>
      </div>

      <div className="grid grid-cols-1 gap-4">
        <Card>
          <CardHeader>
            <CardTitle>營收趨勢</CardTitle>
          </CardHeader>
          <CardContent>
            <ResponsiveContainer width="100%" height={300}>
              <LineChart data={data?.revenue}>
                <CartesianGrid strokeDasharray="3 3" />
                <XAxis dataKey="date" />
                <YAxis />
                <Tooltip />
                <Legend />
                <Line type="monotone" dataKey="amount" stroke="#8884d8" />
              </LineChart>
            </ResponsiveContainer>
          </CardContent>
        </Card>

        <Card>
          <CardHeader>
            <CardTitle>新會員趨勢</CardTitle>
          </CardHeader>
          <CardContent>
            <ResponsiveContainer width="100%" height={300}>
              <LineChart data={data?.newMembers}>
                <CartesianGrid strokeDasharray="3 3" />
                <XAxis dataKey="date" />
                <YAxis />
                <Tooltip />
                <Legend />
                <Line type="monotone" dataKey="count" stroke="#82ca9d" />
              </LineChart>
            </ResponsiveContainer>
          </CardContent>
        </Card>

        <Card>
          <CardHeader>
            <CardTitle>課程參與度趨勢</CardTitle>
          </CardHeader>
          <CardContent>
            <ResponsiveContainer width="100%" height={300}>
              <LineChart data={data?.classAttendance}>
                <CartesianGrid strokeDasharray="3 3" />
                <XAxis dataKey="date" />
                <YAxis />
                <Tooltip />
                <Legend />
                <Line type="monotone" dataKey="count" stroke="#ffc658" />
              </LineChart>
            </ResponsiveContainer>
          </CardContent>
        </Card>
      </div>
    </div>
  );
};

export default Reports;

4. 更新路由

最後,我們需要更新路由設置來包含新的報表頁面。在 src/App.tsx 中:

import React from 'react';
import { BrowserRouter as Router, Route, Routes } from 'react-router-dom';
import MainLayout from './layouts/MainLayout';
import Dashboard from './pages/Dashboard';
import Members from './pages/Members';
import MemberDetail from './pages/MemberDetail';
import Classes from './pages/Classes';
import ClassCalendar from './pages/ClassCalendar';
import ClassDetail from './pages/ClassDetail';
import Reports from './pages/Reports';
import Login from './pages/Login';

function App() {
  return (
    <Router>
      <Routes>
        <Route path="/login" element={<Login />} />
        <Route path="/" element={<MainLayout />}>
          <Route index element={<Dashboard />} />
          <Route path="members" element={<Members />} />
          <Route path="members/:id" element={<MemberDetail />} />
          <Route path="classes" element={<Classes />} />
          <Route path="class-calendar" element={<ClassCalendar />} />
          <Route path="classes/:id" element={<ClassDetail />} />
          <Route path="reports" element={<Reports />} />
        </Route>
      </Routes>
    </Router>
  );
}

export default App;

小結

今天為 Gym Pro 系統新增報表和分析功能,包括:

  1. 在儀表板頁面上展示關鍵績效指標(KPI)和重要圖表。
  2. 建立了一個專門的報表頁面,提供更詳細的數據分析。
  3. 使用 Recharts 庫來創建各種互動式圖表。
  4. 實現了根據不同時間範圍查看報表的功能。

上一篇
[Day 22] Gym Pro:課程管理系統的實作與日曆視圖整合
下一篇
[Day 24] Gym Pro:強化安全性與優化用戶體驗
系列文
React 學得動嗎30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言